from json import loads,dumps
from uuid import uuid1

from django.conf import settings
from django.core import signals
from django.utils.encoding import smart_text
from django.views.decorators.csrf import csrf_exempt
from django.core.serializers.json import DjangoJSONEncoder

from jsonrpc.exceptions import InvalidParamsError, MethodNotFoundError, InvalidRequestError
from jsonrpc.exceptions import OtherError, RequestPostError, Error
from jsonrpc._types import Object, Any, Array

NoneType = type(None)
encode_kw = lambda p: dict([(str(k), v) for k, v in p.items()])# confirm the type of  key is str,for json


def validate_params(method, D):
    if type(D['params']) == Object:
        keys = list(method.json_arg_types.keys())
        if len(keys) != len(D['params']):
            raise InvalidParamsError('Not enough params provided for %s' %
                                     method.json_sig)
        for k in keys:
            if not k in D['params']:
                print('\n\n\n\nSHITTER SHITTER', k, D, '\n\n\n\n')
                raise InvalidParamsError('%s is not a valid parameter for %s' %
                                         (k, method.json_sig))
            if not Any.kind(D['params'][k]) == method.json_arg_types[k]:
                raise InvalidParamsError(
                    '%s is not the correct type %s for %s' %
                    (type(D['params'][k]), method.json_arg_types[k],
                     method.json_sig))
    elif type(D['params']) == Array:
        arg_types = list(method.json_arg_types.values())
        try:
            for i, arg in enumerate(D['params']):
                if not Any.kind(arg) == arg_types[i]:
                    raise InvalidParamsError(
                        '%s is not the correct type %s for %s' %
                        (type(arg), arg_types[i], method.json_sig))
        except IndexError:
            raise InvalidParamsError('Too many params provided for %s' %
                                     method.json_sig)
        else:
            if len(D['params']) != len(arg_types):
                raise InvalidParamsError('Not enough params provided for %s' %
                                         method.json_sig)


class JSONRPCSite(object):
    "A JSON-RPC Site"

    def __init__(self, json_encoder=DjangoJSONEncoder):
        self.urls = {}
        self.uuid = str(uuid1())
        self.version = '2.0'
        self.name = 'django-json-rpc'
        self.set_json_encoder(json_encoder)

    def set_json_encoder(self, json_encoder=DjangoJSONEncoder):
        self.json_encoder = json_encoder

    def register(self, name, method):
        self.urls[smart_text(name)] = method

    def empty_response(self):
        resp = {'id': None,'jsonrpc':'2.0'}
        resp.update({'error': None, 'result': None})
        return resp

    def response_dict(self, request, D,is_batch=False,version_hint='2.0',json_encoder=None):
        json_encoder = json_encoder or self.json_encoder
        version = version_hint
        response = self.empty_response()
        apply_version = lambda f, r, p: f(r, **encode_kw(p)) if type(p) is dict else f(r, *p)

        try:
            # params: An Array or Object, that holds the actual parameter values
            # for the invocation of the procedure. Can be omitted if empty.
            if 'params' not in D:
                D['params'] = []
            if 'method' not in D or 'params' not in D:
                raise InvalidParamsError(
                    'Request requires str:"method" and list:"params"')
            if D['method'] not in self.urls:
                raise MethodNotFoundError(
                    'Method not found. Available methods: %s' % (
                        '\n'.join(self.urls.keys())))

            method = self.urls[str(D['method'])]
            if getattr(method, 'json_validate', False):
                validate_params(method, D)

            if 'id' in D and D['id'] is not None:  # regular request
                response['id'] = D['id']
                if 'error' in response:
                    response.pop('error')
            elif is_batch:  # notification, not ok in a batch format, but happened anyway
                raise InvalidRequestError

            R = apply_version(method, request, D['params'])

            if 'id' not in D or ('id' in D and D['id'] is None):  # notification
                return None, 204

            if isinstance(R, tuple):
                R = list(R)

            encoder = json_encoder()
            builtin_types = (dict, list, set, NoneType, bool, int,str) 
            if all(not isinstance(R, e) for e in builtin_types):
                try:
                    _ = encoder.default(R)  # ...or something this thing supports
                except TypeError as exc:
                    raise TypeError("Return type not supported, for %r" % R)
            response['result'] = R

            status = 200

        except Error as e:
            response['error'] = e.json_rpc_format
            if version in ('1.1', '2.0') and 'result' in response:
                response.pop('result')
            status = e.status
        except Exception as e:
            # exception missed by others
            signals.got_request_exception.send(sender=self.__class__,
                                               request=request)

            # Put stacktrace into the OtherError only if DEBUG is enabled
            if settings.DEBUG:
                other_error = OtherError(e)
            else:
                other_error = OtherError("Internal Server Error")

            response['error'] = other_error.json_rpc_format
            status = other_error.status
            if version in ('1.1', '2.0') and 'result' in response:
                response.pop('result')

        # Exactly one of result or error MUST be specified. It's not
        # allowed to specify both or none.
        if version in ('1.1', '2.0'
                   ) and 'error' in response and not response['error']:
            response.pop('error')

        return response, status

    @csrf_exempt
    def dispatch(self, request, method='', json_encoder=None):
        from django.http import HttpResponse
        json_encoder = json_encoder or self.json_encoder
        try:
            # in case we do something json doesn't like, we always get back valid json-rpc response
            response = self.empty_response()
            if not request.method.lower() == 'post': # not support get method
                raise RequestPostError
            
            try:
                if hasattr(request, "body"):
                    D = loads(request.body.decode('utf-8'))
                else:
                    D = loads(request.raw_post_data.decode('utf-8'))
            except:
                raise InvalidRequestError
            if type(D) is list:
                response = [self.response_dict(request, d,is_batch=True,json_encoder=json_encoder)[0]
                            for d in D]
                status = 200
            else:
                response, status = self.response_dict(request, D,json_encoder=json_encoder)
                if response is None and (not 'id' in D or D['id'] is None):  # a notification
                    return HttpResponse('', status=status)

            json_rpc = dumps(response, cls=json_encoder)
        except Error as e:
            response['error'] = e.json_rpc_format
            status = e.status
            json_rpc = dumps(response, cls=json_encoder)
        except Exception as e:
            # exception missed by others
            signals.got_request_exception.send(sender=self.__class__,
                                               request=request)
            # Put stacktrace into the OtherError only if DEBUG is enabled
            if settings.DEBUG:
                other_error = OtherError(e)
            else:
                other_error = OtherError("Internal Server Error")

            response['result'] = None
            response['error'] = other_error.json_rpc_format
            status = other_error.status
            json_rpc = dumps(response, cls=json_encoder)
        return HttpResponse(json_rpc,
                            status=status,
                            content_type='application/json-rpc')
jsonrpc_site = JSONRPCSite()
